package com.hwtx.form.domain.handle.core;

import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.hwtx.form.domain.FormConstants;
import com.hwtx.form.domain.def.FormDef;
import com.hwtx.form.domain.def.meta.config.FormComponentConfig;
import com.hwtx.form.domain.ds.DataSourceContextHolder;
import com.hwtx.form.domain.handle.SelectItemsInfo;
import com.hwtx.form.domain.handle.datamodel.DataModel;
import com.hwtx.form.domain.handle.datamodel.DataModelDetail;
import com.hwtx.form.domain.handle.datamodel.DataModelRelate;
import com.hwtx.form.domain.listener.FormDataChangeEvent;
import com.hwtx.form.domain.listener.FormDataChangeInterceptor;
import com.hwtx.form.domain.repo.DataModelRepo;
import com.hwtx.form.domain.vo.DataModelDetailQuery;
import com.hwtx.form.domain.vo.FormData;
import com.hwtx.form.domain.vo.ResultVo;
import com.hwtx.form.persistence.DataSourceRepo;
import com.hwtx.form.persistence.entity.FormDefDetail;
import com.hwtx.form.persistence.entity.I18nMessage;
import com.hwtx.form.util.JacksonUtil;
import io.geekidea.boot.framework.exception.BusinessException;
import jakarta.annotation.Resource;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.hwtx.form.domain.FormConstants.DefaultColumn.*;
import static com.hwtx.form.domain.FormConstants.OPERATION_ADD;
import static com.hwtx.form.domain.FormConstants.OPERATION_EDIT;
import static com.hwtx.form.util.FormUtil.getLongPostValue;

@Component
@Slf4j
public class DefaultFormDataWriteHandle extends DefaultFormBaseHandle {

    SelectItemsInfo.ComponentItemParser parser = new SelectItemsInfo.DefaultComponentItemParser();
    @Resource
    private DataModelRepo dataModelRepo;
    @Resource
    private DataSourceRepo dataSourceRepo;
    @Resource
    private JdbcTemplate jdbcTemplate;
    @Resource
    private List<FormDataChangeInterceptor> interceptors;

    @Transactional
    public Object saveFormData(FormDef formDef, FormData formData) throws Exception {
        Map<String, Object> data = formData.getData();
        handleI18n(formDef, JacksonUtil.map2Oject(data, I18nMessage.class));
        Long id = getLongPostValue(data, FormConstants.DefaultColumn.id.name());
        SelectItemsInfo itemInfo = getFormItems(formDef.getFormId(), true);
        FormDefDetail formDefDetail = formConfigMeta.getFormDefDetail(formDef.getFormId());
        String modelName = formDefDetail.getModelName();
        DataModel dataModel = dataModelRepo.getDataModel(modelName);
        if (dataModel == null) {
            throw new RuntimeException("数据模型不存在");
        }
        List<DataModelRelate> relates = dataModelRepo.getRelates(modelName);
        Map<String, Set<String>> tableColumns = Maps.newHashMap();
        if (CollectionUtils.isNotEmpty(relates)) {
            tableColumns.put(dataModel.getMaster(), dataSourceRepo.getColumnNames(dataModel.getMaster()));
            relates.forEach(relate -> tableColumns.put(relate.getSecondName(), dataSourceRepo.getColumnNames(relate.getSecondName())));
        }
        Map<String, Map<String, String>> uniqueItems = itemInfo.getUniqueItems();
        if (MapUtils.isNotEmpty(uniqueItems)) {
            for (Map.Entry<String, Map<String, String>> entry : uniqueItems.entrySet()) {
                String table = entry.getKey();
                for (String uniqueItem : entry.getValue().keySet()) {
                    String selectIdSql = dataSourceRepo.buildSelect(dataModel, relates, Map.of(table, uniqueItem), tableColumns);
                    List<Long> ids = jdbcTemplate.queryForList(selectIdSql, Long.class, data.get(uniqueItem));
                    boolean allow = ids.isEmpty();
                    if (id != null && !allow) {
                        allow = Objects.equals(ids.getFirst(), id);
                    }
                    if (!allow) {
                        return ResultVo.builder().success(false).msg("字段 【" + entry.getValue().get(uniqueItem) + "】不允许重复").build();
                    }
                }
            }
        }
        try {
            if (id == null || StringUtils.isBlank(id.toString().trim())) {
                if (CollectionUtils.isEmpty(relates)) {
                    List<SelectItemsInfo.SelectItemInfo> items = itemInfo.getNoTableItems();
                    if (CollectionUtils.isNotEmpty(itemInfo.getTableItems().get(dataModel.getMaster()))) {
                        items.addAll(itemInfo.getTableItems().get(dataModel.getMaster()));
                    }
                    InsertInfo insertInfo = saveBySingleTable(dataModel, items, dataModel.getMaster(), null, data, formDef);
                    if (insertInfo != null) {
                        if (insertInfo.msg != null) {
                            return ResultVo.builder().success(false).msg(insertInfo.msg).build();
                        }
                        return ResultVo.builder().success(true).data(insertInfo.key).build();
                    }
                    return ResultVo.builder().success(false).msg("保存失败").build();
                }
                return ResultVo.builder().success(true).data(saveByAggTable(dataModel, relates, itemInfo, data, formDef)).build();
            } else {
                if (CollectionUtils.isEmpty(relates)) {
                    List<SelectItemsInfo.SelectItemInfo> items = itemInfo.getNoTableItems();
                    if (CollectionUtils.isNotEmpty(itemInfo.getTableItems().get(dataModel.getMaster()))) {
                        items.addAll(itemInfo.getTableItems().get(dataModel.getMaster()));
                    }
                    Object ret = updateSingleTable(dataModel, items, dataModel.getMaster(), data, formDef, id);
                    if (ret != null) {
                        return ResultVo.builder().success(false).msg(ret.toString()).build();
                    }
                    return ResultVo.builder().success(true).data(id).build();
                }
                updateByAggTable(dataModel, relates, itemInfo, data, formDef);
                return ResultVo.builder().success(true).data(id).build();
            }
        } finally {
            DataSourceContextHolder.clearDataSourceKey();
        }
    }

    private boolean saveByAggTable(DataModel dataModel, List<DataModelRelate> relates, SelectItemsInfo itemsInfo, Map<String, Object> data, FormDef formDef) {
        List<String> hasHandleTables = Lists.newArrayList();
        for (int i = relates.size() - 1; i >= 0; i--) {
            DataModelRelate relate = relates.get(i);
            List<DataModelRelate.RelateField> fields = JacksonUtil.json2Array(relate.getJoinFields(), new TypeReference<>() {
            });
            String leftTable = relate.getFirstName();
            String rightTable = relate.getSecondName();
            for (DataModelRelate.RelateField field : fields) {
                InsertInfo insertInfo;
                boolean leftIsPri = Objects.equals(dataSourceRepo.getColumns(leftTable).get(field.getFirstField()).isPrimaryKey(), 1);
                boolean rightIsPri = Objects.equals(dataSourceRepo.getColumns(rightTable).get(field.getSecondField()).isPrimaryKey(), 1);
                if (!leftIsPri && !rightIsPri) {
                    continue;
                }
                if (leftIsPri) {
                    if (!hasHandleTables.contains(leftTable)) {
                        insertInfo = handleTable(dataModel, itemsInfo.getTableItems().get(leftTable), leftTable, null, data, formDef);
                        handleTable(dataModel, itemsInfo.getTableItems().get(rightTable), rightTable, Map.of(field.getSecondField(), insertInfo.key), data, formDef);
                        hasHandleTables.add(leftTable);
                    }
                } else {
                    if (!hasHandleTables.contains(rightTable)) {
                        insertInfo = handleTable(dataModel, itemsInfo.getTableItems().get(rightTable), rightTable, null, data, formDef);
                        handleTable(dataModel, itemsInfo.getTableItems().get(leftTable), leftTable, Map.of(field.getFirstField(), insertInfo.key), data, formDef);
                        hasHandleTables.add(rightTable);
                    }
                }
            }
        }
        itemsInfo.getNoTableItems().forEach(item -> {
            if (item.getFormComponentConfig() != null) {
                item.getFormComponentConfig().invokeWriteService(item.getComExt(), item, data);
            }
        });
        return true;
    }

    private InsertInfo handleTable(DataModel dataModel, List<SelectItemsInfo.SelectItemInfo> handleItems, String table, Map<String, Object> joinKeyAndVal, Map<String, Object> data, FormDef formDef) {
        InsertInfo insertInfo = saveBySingleTable(dataModel, handleItems, table, joinKeyAndVal, data, formDef);
        if (insertInfo == null) {
            log.error("保存失败,table = {}", table);
            throw new BusinessException("保存失败");
        } else if (insertInfo.msg != null) {
            throw new BusinessException(insertInfo.msg);
        }
        return insertInfo;
    }

    private InsertInfo saveBySingleTable(DataModel dataModel, List<SelectItemsInfo.SelectItemInfo> handleItems, String table, Map<String, Object> joinKeyAndVal, Map<String, Object> data, FormDef formDef) {
        Map<String, DataModelDetail> details = dataModelRepo.getDetails(DataModelDetailQuery.builder().modelName(dataModel.getModelName()).tableName(table).status(FormConstants.Status.VALID).build()).stream().collect(Collectors.toMap(DataModelDetail::getName, Function.identity()));
        List<String> columns = Lists.newArrayList();
        List<Object> parameters = Lists.newArrayList();
        handleItems.sort((item1, item2) -> {
            if (item1.isApiInvoke() && !item2.isApiInvoke()) {
                return 1;
            }
            if (!item1.isApiInvoke() && item2.isApiInvoke()) {
                return -1;
            }
            return 0;
        });
        Map<String, Integer> fieldValueIndex = Maps.newHashMap();
        AtomicInteger valueIndex = new AtomicInteger(0);
        handleItems.forEach(item -> {
            if (Objects.equals(item.getField(), id.name())) {
                return;
            }
            List<String> fields = parser.updateFields(item, table);
            if (CollectionUtils.isNotEmpty(fields)) {
                fields.stream().filter(field -> !fieldValueIndex.containsKey(field)).forEach(field -> {
                    columns.add(field);
                    if (!item.isApiInvoke()) {
                        fieldValueIndex.put(field, valueIndex.getAndIncrement());
                    }
                });

                if (item.isApiInvoke()) {
                    if (item.getFormComponentConfig() != null) {
                        FormComponentConfig.ComponentParamInfo componentParamInfo = item.getFormComponentConfig().getInvokeComponentParamInfo(item.getComExt());
                        if (componentParamInfo != null) {
                            List<FormComponentConfig.ParamMapping> inputs = componentParamInfo.getInput();
                            if (CollectionUtils.isNotEmpty(inputs)) {
                                Map<String, Object> inputValues = Maps.newHashMap();
                                inputs.forEach(input -> {
                                    String name = input.getAlias();
                                    if (StringUtils.isBlank(name)) {
                                        name = input.getKey();
                                    }
                                    if (fieldValueIndex.containsKey(name)) {
                                        inputValues.put(input.getKey(), parameters.get(fieldValueIndex.get(name)));
                                    } else {
                                        inputValues.put(input.getKey(), data.get(name));
                                    }
                                });
                                Map<String, Object> invokeRet = parser.getInvokeValues(item, componentParamInfo.getCode(), inputValues);
                                if (invokeRet != null && componentParamInfo.getOutput() != null) {
                                    componentParamInfo.getOutput().forEach(output -> {
                                        String name = output.getKey();
                                        if (StringUtils.isNotBlank(output.getAlias())) {
                                            name = output.getAlias();
                                        }
                                        if (fieldValueIndex.containsKey(name)) {
                                            parameters.set(fieldValueIndex.get(name), invokeRet.get(output.getKey()));
                                        } else {
                                            parameters.add(invokeRet.get(output.getKey()));
                                            valueIndex.getAndIncrement();
                                        }
                                    });
                                }
                            }
                        }
                    }
                } else {
                    parser.updateValues(data, item, table, field -> {
                        if (details.containsKey(field)) {
                            return details.get(field).getDefValue();
                        }
                        return null;
                    }).forEach((k, v) -> {
                        Integer index = fieldValueIndex.get(k);
                        if (index != null && index < parameters.size()) {
                            parameters.set(fieldValueIndex.get(k), v);
                        } else {
                            parameters.add(v);
                        }
                    });
                }
            }
            if (item.getFormComponentConfig() != null) {
                item.getFormComponentConfig().invokeWriteService(item.getComExt(), item, data);
            }
        });
        if (joinKeyAndVal != null) {
            joinKeyAndVal.forEach((k, v) -> {
                columns.add(k);
                parameters.add(v);
            });
        }
        boolean hasDefaultColumns = dataModelRepo.hasFields(dataModel.getModelName(), table, getDefaultColumns().toArray());
        String insertSql = dataSourceRepo.buildInsertSql(table, columns, hasDefaultColumns);
        if (hasDefaultColumns) {
            parameters.addAll(getCreateDefaultColumns().values());
        }
        KeyHolder keyHolder = new GeneratedKeyHolder();
        checkJdbcTemplate(dataModel.getDs());
        boolean ret = jdbcTemplate.update(conn -> {
            PreparedStatement ps = conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS);
            AtomicInteger index = new AtomicInteger(1);
            parameters.forEach(param -> {
                try {
                    ps.setObject(index.getAndIncrement(), param);
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            });
            return ps;
        }, keyHolder) > 0;
        if (ret) {
            for (FormDataChangeInterceptor interceptor : interceptors) {
                Object result = interceptor.after(FormDataChangeEvent.builder().type(OPERATION_ADD).handleItems(Lists.newArrayList(columns)).changeFormId(formDef.getFormId()).data(parameters).dataModel(dataModel).build());
                if (result != null) {
                    return InsertInfo.builder().msg(result.toString()).build();
                }
            }
        }
        if (keyHolder.getKey() != null) {
            return InsertInfo.builder().key(keyHolder.getKey().longValue()).columns(columns).parameters(parameters).build();
        }
        return null;
    }

    private void updateByAggTable(DataModel dataModel, List<DataModelRelate> relates, SelectItemsInfo itemsInfo, Map<String, Object> data, FormDef formDef) {
        List<String> hasHandleTables = Lists.newArrayList();
        for (int i = relates.size() - 1; i >= 0; i--) {
            DataModelRelate relate = relates.get(i);
            String leftTable = relate.getFirstName();
            String rightTable = relate.getSecondName();
            if (!hasHandleTables.contains(leftTable)) {
                Long leftTableId = getLongPostValue(data, leftTable + "_id");
                if (leftTableId != null) {
                    updateSingleTable(dataModel, itemsInfo.getTableItems().get(leftTable), leftTable, data, formDef, leftTableId);
                }
                hasHandleTables.add(leftTable);
            }
            if (!hasHandleTables.contains(rightTable)) {
                Long rightTableId = getLongPostValue(data, rightTable + "_id");
                if (rightTableId != null) {
                    updateSingleTable(dataModel, itemsInfo.getTableItems().get(rightTable), rightTable, data, formDef, rightTableId);
                }
                hasHandleTables.add(rightTable);
            }
        }
        itemsInfo.getNoTableItems().forEach(item -> {
            if (item.getFormComponentConfig() != null) {
                item.getFormComponentConfig().invokeWriteService(item.getComExt(), item, data);
            }
        });
    }

    private Object updateSingleTable(DataModel dataModel, List<SelectItemsInfo.SelectItemInfo> handleItems, String table, Map<String, Object> data, FormDef formDef, Object id) {
        List<String> columns = Lists.newArrayList();
        List<Object> parameters = Lists.newArrayList();
        handleItems.forEach(item -> {
            if (item.isMask()) {
                return;
            }
            Object value = null;
            if (StringUtils.isNotBlank(item.getName())) {
                value = data.get(item.getName());
            }
            if (value == null) {
                value = data.get(item.getField());
            }
            if (value != null) {
                List<String> fields = parser.updateFields(item, table);
                if (fields != null) {
                    Map<String, Object> fieldValues = parser.updateValues(data, item, table, null);
                    fields.forEach(field -> {
                        columns.add(field);
                        parameters.add(fieldValues.get(field));
                    });
                }
            }
            if (item.getFormComponentConfig() != null) {
                item.getFormComponentConfig().invokeWriteService(item.getComExt(), item, data);
            }
        });
        boolean hasDefaultColumns = dataModelRepo.hasFields(dataModel.getModelName(), table, getDefaultColumns().toArray());
        if (hasDefaultColumns) {
            parameters.addAll(getModifyDefaultColumns().values());
        }
        parameters.add(id);
        String updateSql = dataSourceRepo.buildUpdateSql(table, columns, Lists.newArrayList(FormConstants.DefaultColumn.id.name()), hasDefaultColumns);
        checkJdbcTemplate(dataModel.getDs());
        boolean ret = jdbcTemplate.update(updateSql, parameters.toArray()) > 0;
        if (ret) {
            for (FormDataChangeInterceptor interceptor : interceptors) {
                Object result = interceptor.after(FormDataChangeEvent.builder().type(OPERATION_EDIT).handleItems(columns).changeFormId(formDef.getFormId()).data(parameters).dataModel(dataModel).build());
                if (result != null) {
                    return result.toString();
                }
            }
        }
        return null;
    }

    private void handleI18n(FormDef formDef, I18nMessage i18nMessage) {
        if (!formDef.getName().equalsIgnoreCase("form-i18n")) {
            return;
        }
        handleI18n(i18nMessage);
    }

    @Builder
    @Getter
    static class InsertInfo {
        List<String> columns;
        List<Object> parameters;
        Long key;
        String msg;
    }
}
